﻿/*
   Copyright 2012 Dmitry Bratus

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading;
using System.ServiceModel;
using Flower.Directory.Util;
using Flower.Logging;
using Flower.Services;
using Flower.Services.Data;
using Flower.Directory.Scripting;
using Flower.Directory.Host;

namespace Flower.MemoryDirectory
{
    public class Directory : IDirectory, IProcessesDispatcherHolder
    {
        private static readonly Log Log = LogManager.CreateLog("MemoryDirectory");

        private readonly bool _emulateSerialization;
        private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
        private readonly DirectoryEntryContainer _root = new DirectoryEntryContainer();
        private readonly Dictionary<string, string> _idMap = new Dictionary<string, string>();
        private readonly IDirectory _selfProxy;
        private readonly IProcessesDispatcher _processesDispatcher;
        private readonly ScriptRunner _scriptRunner;

        private long _idCounter = 0;

        public IProcessesDispatcher ProcessesDispatcher
        {
            get { return _processesDispatcher; }
        }

        #region Initialization

        public Directory
        (
            IScriptEnginePool scriptEnginePool, 
            IDirectoryProxyProvider directoryProxyProvider = null,
            IProcessesDispatcher processesDispatcher = null,
            bool emulateSerialization = true
        )
        {
            _processesDispatcher = processesDispatcher;
            _emulateSerialization = emulateSerialization;

            if (_processesDispatcher != null)
            {
                _processesDispatcher.Directory = this;
            }

            Log.Trace("Creating directory");

            DateTime now = DateTime.Now;
            _root.Entry =
                new Folder
                {
                    Id = DirectoryUtil.MakeId(_idCounter++),
                    Name = string.Empty,
                    CreatedOn = now,
                    UpdatedOn = now
                };

            _idMap.Add(_root.Entry.Id, "/");

            _selfProxy = (directoryProxyProvider != null) ? directoryProxyProvider.MakeProxy(this) : this;
            _selfProxy.Save("/", new Folder { Name = "Assemblies" });
            _selfProxy.Save("/", new Folder { Name = "Services" });
            _selfProxy.Save("/", new Folder { Name = "Workflows" });
            _selfProxy.Save("/", new Folder { Name = "Processes" });
            _selfProxy.Save("/", new Folder { Name = "Processors" });
            _selfProxy.Save("/", new Folder { Name = "Sets" });
            _selfProxy.Save("/Sets", new Folder { Name = "Local" });
            _selfProxy.Save("/Sets", new Folder { Name = "Shared" });
            _selfProxy.Save("/", new Folder { Name = "Roles" });
            _selfProxy.Save("/Roles", new Role { Name = "Application" });
            _selfProxy.Save("/Roles", new Role { Name = "Processor" });
            _selfProxy.Save("/Roles", new Role { Name = "Administrator" });

            _scriptRunner = new ScriptRunner(scriptEnginePool, _selfProxy);
        }

        #endregion

        #region IDirectory members

        public string GetPathById(string id)
        {
            _lock.EnterReadLock();
            try
            {
                string path;

                if (!_idMap.TryGetValue(id, out path))
                {
                    throw new FaultException<DirectoryFault>
                    (
                        new DirectoryFault 
                        {
                            Code = DirectoryFaultCode.EntryNotFound
                        },
                        "Entry with the specified id doesn't exist."
                    );
                }

                return path;
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }

        public DirectoryEntry Get(string what)
        {
            _lock.EnterReadLock();
            try
            {
                string path = (DirectoryUtil.IsId(what)) ? GetPathById(what) : what;

                DirectoryEntryContainer ent = _root.GetRecursively(DirectoryUtil.SplitPath(path), 0);

                return (ent != null) ? (_emulateSerialization) ? ent.Entry.Clone() : ent.Entry : null;
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }

        public DirectoryEntryInfo GetInfo(string what)
        {
            return EntryToInfo(Get(what));
        }

        public IList<DirectoryEntry> GetChildren(string parent, DirectoryEntryTypes entryTypes, SortProperty property, SortOrder order, long offset, long limit)
        {
            _lock.EnterReadLock();
            try
            {
                string path = (DirectoryUtil.IsId(parent)) ? GetPathById(parent) : parent;

                var children = new List<DirectoryEntry>();

                DirectoryEntryContainer parentEnt = _root.GetRecursively(DirectoryUtil.SplitPath(path), 0);
                
                if (parentEnt != null)
                {
                    foreach (DirectoryEntryContainer directoryEntryContainer in SortAndLimit(parentEnt.Children, entryTypes, property, order, offset, limit))
                    {
                        children.Add((_emulateSerialization) ? directoryEntryContainer.Entry.Clone() : directoryEntryContainer.Entry);
                    }
                }

                return children;
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }

        public IList<DirectoryEntryInfo> GetChildrenInfo(string parent, DirectoryEntryTypes entryTypes, SortProperty property, SortOrder order, long offset, long limit)
        {
            return GetChildren(parent, entryTypes, property, order, offset, limit).Select(EntryToInfo).ToList();
        }

        public long CountChildren(string parent, DirectoryEntryTypes entryTypes)
        {
            _lock.EnterReadLock();
            try
            {
                string path = (DirectoryUtil.IsId(parent)) ? GetPathById(parent) : parent;

                DirectoryEntryContainer parentEnt = _root.GetRecursively(DirectoryUtil.SplitPath(path), 0);

                return parentEnt != null ? parentEnt.Children.Count(ent => entryTypes.HasFlag(ent.EntryType)) : 0;
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }

        public DirectoryEntry GetAncestor(string child, int n, DirectoryEntryTypes ancestorEntryTypes)
        {
            _lock.EnterReadLock();
            try
            {
                string path = (DirectoryUtil.IsId(child)) ? GetPathById(child) : child;

                int depth;
                DirectoryEntryContainer lastExistingEnt = _root.GetDeepestExistingRecursively(DirectoryUtil.SplitPath(path), 0, out depth);
                DirectoryEntryContainer ancEnt = lastExistingEnt.GetAncestor(ancestorEntryTypes, n);

                return (ancEnt != null) ? (_emulateSerialization) ? ancEnt.Entry.Clone() : ancEnt.Entry : null;
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }

        public DirectoryEntryInfo GetAncestorInfo(string child, int n, DirectoryEntryTypes ancestorEntryTypes)
        {
            return EntryToInfo(GetAncestor(child, n, ancestorEntryTypes));
        }

        public string Save(string parent, DirectoryEntry entry)
        {
            _lock.EnterWriteLock();
            try
            {
                DirectoryEntryContainer parentEnt = null;

                if (parent != null)
                {
                    string path = (DirectoryUtil.IsId(parent)) ? GetPathById(parent) : parent;
                    parentEnt = AppendNonExistingParts(DirectoryUtil.SplitPath(path));
                }
                
                if (_emulateSerialization)
                {
                    entry = entry.Clone();
                }

                if (entry.Id == null)
                {
                    if (parentEnt == null)
                    {
                        throw new InvalidOperationException("For new entries the parent must be specified.");
                    }

                    if (parentEnt.Children.Any(chld => chld.Entry.Name == entry.Name))
                    {
                        throw new FaultException<DirectoryFault>
                        (
                            new DirectoryFault
                            {
                                Code = DirectoryFaultCode.NameCollision
                            },
                            "Parent entry already contains an entry with the same name."
                        );
                    }

                    entry.Id = DirectoryUtil.MakeId(_idCounter++);

                    if (entry.Name == null)
                    {
                        entry.Name = entry.Id;
                    }

                    entry.CreatedOn = entry.UpdatedOn = DateTime.Now;

                    var newEnt = new DirectoryEntryContainer
                    {
                        Parent = parentEnt,
                        Entry = entry
                    };
                    parentEnt.Children.Add(newEnt);

                    _idMap[entry.Id] = newEnt.GetPath();
                }
                else
                {
                    DirectoryEntryContainer ent = 
                        _root.GetRecursively(DirectoryUtil.SplitPath(GetPathById(entry.Id)), 0);

                    ent.Entry = entry;
                    entry.UpdatedOn = DateTime.Now;

                    if (parentEnt != null && parentEnt.Entry.Id != ent.Parent.Entry.Id)
                    {
                        Move(entry.Id, parentEnt.Entry.Id);
                    }
                }

                return entry.Id;
            }
            finally
            {
                _lock.ExitWriteLock();
            }
        }

        public void Move(string what, string where)
        {
            _lock.EnterWriteLock();
            try
            {
                string path = (DirectoryUtil.IsId(what)) ? GetPathById(what) : what;

                DirectoryEntryContainer whatEnt = _root.GetRecursively(DirectoryUtil.SplitPath(path), 0);

                if (whatEnt == null)
                {
                    throw new FaultException<DirectoryFault>
                    (
                        new DirectoryFault
                        {
                            Code = DirectoryFaultCode.EntryNotFound
                        },
                        "Entry with the specified id or path doesn't exist."
                    );
                }

                path = (DirectoryUtil.IsId(where)) ? GetPathById(where) : where;

                DirectoryEntryContainer whereEnt = AppendNonExistingParts(DirectoryUtil.SplitPath(path));

                if (whereEnt.Children.Any(chld => chld.Entry.Name == whatEnt.Entry.Name))
                {
                    throw new FaultException<DirectoryFault>
                    (
                        new DirectoryFault
                        {
                            Code = DirectoryFaultCode.NameCollision
                        },
                        "Destination already contains an entry with the same name."
                    );
                }

                whatEnt.RemoveFromParent(id => _idMap.Remove(id));
                whereEnt.Children.Add(whatEnt);
                whatEnt.Parent = whereEnt;

                whereEnt.Entry.UpdatedOn = DateTime.Now;

                _idMap[whatEnt.Entry.Id] = whatEnt.GetPath();
            }
            finally
            {
                _lock.ExitWriteLock();
            }
        }

        public void Remove(string what)
        {
            _lock.EnterWriteLock();
            try
            {
                string path = (DirectoryUtil.IsId(what)) ? GetPathById(what) : what;

                DirectoryEntryContainer whatEnt = _root.GetRecursively(DirectoryUtil.SplitPath(path), 0);

                if (whatEnt != null)
                {
                    whatEnt.RemoveFromParent(id => _idMap.Remove(id));
                }
            }
            finally
            {
                _lock.ExitWriteLock();
            }
        }

        public bool Exists(string what, DateTime timestamp)
        {
            _lock.EnterReadLock();
            try
            {
                string path;
                if (DirectoryUtil.IsId(what))
                {
                    if (!_idMap.TryGetValue(what, out path))
                    {
                        return false;
                    }
                }
                else
                {
                    path = what;
                }

                DirectoryEntryContainer ent = _root.GetRecursively(DirectoryUtil.SplitPath(path), 0);

                return ent != null && ent.Entry.UpdatedOn > timestamp;
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }

        public ICollection<ScriptParameter> ExecuteScript(string script, ICollection<ScriptParameter> args)
        {
            return _scriptRunner.ExecuteScript(string.Empty, script, args);
        }

        #endregion

        #region Misc

        private DirectoryEntryContainer AppendNonExistingParts(string[] path)
        {
            if (path.Length == 0)
            {
                return _root;
            }

            int depth;
            DirectoryEntryContainer lastExistingEnt = _root.GetDeepestExistingRecursively(path, 0, out depth);

            for (int i = depth; i < path.Length; i++)
            {
                var now = DateTime.Now;
                string newEntId = DirectoryUtil.MakeId(_idCounter++);
                var newEnt = new DirectoryEntryContainer 
                {
                    Parent = lastExistingEnt,
                    Entry = new Folder 
                    {
                        Id = newEntId,
                        Name = path[i],
                        CreatedOn = now,
                        UpdatedOn = now
                    }
                };

                lastExistingEnt.Children.Add(newEnt);
                lastExistingEnt = newEnt;

                _idMap[newEntId] = newEnt.GetPath();
            }

            return lastExistingEnt;
        }

        private DirectoryEntryInfo EntryToInfo(DirectoryEntry entry)
        {
            if (entry == null) return null;

            DirectoryEntryTypes type;

            if (entry is Assembly)
            {
                type = DirectoryEntryTypes.Assembly;
            }
            else if (entry is Folder)
            {
                type = DirectoryEntryTypes.Folder;
            }
            else if (entry is Link)
            {
                type = DirectoryEntryTypes.Link;
            }
            else if (entry is Message)
            {
                type = DirectoryEntryTypes.Message;
            }
            else if (entry is Process)
            {
                type = DirectoryEntryTypes.Process;
            }
            else if (entry is Processor)
            {
                type = DirectoryEntryTypes.Processor;
            }
            else if (entry is Role)
            {
                type = DirectoryEntryTypes.Role;
            }
            else if (entry is ServiceContainer)
            {
                type = DirectoryEntryTypes.ServiceContainer;
            }
            else if (entry is Set)
            {
                type = DirectoryEntryTypes.Set;
            }
            else if (entry is State)
            {
                type = DirectoryEntryTypes.State;
            }
            else if (entry is Workflow)
            {
                type = DirectoryEntryTypes.Workflow;
            }
            else
            {
                throw new InvalidOperationException(string.Format("Unknown entry type {0}.", entry.GetType().FullName));
            }

            return new DirectoryEntryInfo 
            {
                Id = entry.Id,
                Name = entry.Name,
                AccessControlList = entry.AccessControlList,
                CreatedOn = entry.CreatedOn,
                UpdatedOn = entry.UpdatedOn,
                Type = type
            };
        }

        #endregion

        #region Sorting and limits

        private static IEnumerable<DirectoryEntryContainer> SortAndLimit(IEnumerable<DirectoryEntryContainer> entries, DirectoryEntryTypes entryTypes, SortProperty property, SortOrder order, long offset, long limit)
        {
            IEnumerable<DirectoryEntryContainer> sortedEntries;

            switch (property)
            {
                case SortProperty.Default:
                    sortedEntries = 
                        (order == SortOrder.Asc) ? 
                            entries : 
                            entries.Reverse();
                    break;
                case SortProperty.Name:
                    sortedEntries =
                        (order == SortOrder.Asc) ?
                            entries.OrderBy(ent => ent.Entry.Name) :
                            entries.OrderByDescending(ent => ent.Entry.Name);
                    break;
                case SortProperty.UpdatedOn:
                    sortedEntries =
                        (order == SortOrder.Asc) ?
                            entries.OrderBy(ent => ent.Entry.UpdatedOn) :
                            entries.OrderByDescending(ent => ent.Entry.UpdatedOn);
                    break;
                case SortProperty.CreatedOn:
                    sortedEntries =
                        (order == SortOrder.Asc) ?
                            entries.OrderBy(ent => ent.Entry.CreatedOn) :
                            entries.OrderByDescending(ent => ent.Entry.CreatedOn);
                    break;
                default:
                    sortedEntries = entries;
                    break;
            }

            long i = 0;

            foreach (DirectoryEntryContainer entry in sortedEntries)
            {
                if (entryTypes.HasFlag(entry.EntryType))
                {
                    if (i >= offset)
                    {
                        yield return entry;
                    }

                    ++i;

                    if (i == limit)
                    {
                        break;
                    }
                }
            }
        }

        #endregion
    }
}
